Skip to content

fix(openclaw): skip conversational opt-out directives and inline secrets in shouldCapture - #1355

Open
harrymove-ctrl wants to merge 11 commits into
devfrom
fix/openclaw-capture-optout-and-secrets
Open

harrymove-ctrl wants to merge 11 commits into
devfrom
fix/openclaw-capture-optout-and-secrets

Conversation

@harrymove-ctrl

@harrymove-ctrl harrymove-ctrl commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add looksLikeOptOut and looksLikeSecret checks in packages/openclaw-memory-memwal/src/capture.ts ahead of TRIGGER_PATTERNS so explicit conversational opt-out directives (such as do not remember, don't save, off the record, [no-save]) and inline secret patterns (suiprivkey1, vendor API keys, PEM blocks, Bearer headers, and labelled 64-hex keys) are rejected locally before registerCaptureHook calls client.analyze().
  • Add unit and hook-level regression tests in packages/openclaw-memory-memwal/test/capture-optout.test.mjs.

Verification

  • Reproduced with @mysten-incubation/dev-wallet before and after the fix: shouldCapture now returns false for both opt-out and secret turns and registerCaptureHook sends 0 requests to /api/analyze.
  • pnpm --dir packages/openclaw-memory-memwal test (48 passed).

cc @ducnmm

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm October 29, 2026 09:54
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

@ducnmm This PR is ready for review at commit 5059165. All 22/22 CI checks are passing.

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tightened opt-out patterns now miss ordinary explicit requests not to save a turn. Offline probes of the current capture module return hasCaptureOptOut=false and shouldCapture=true for the examples below; full package/live tests were not rerun.

"i",
),
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:save|store|record|capture|keep)(?:\\s+(?:this|that|these|those|it|anything|what\\s+i|my\\b|our\\b|(?:in|to)\\s+memory|the\\s+(?:conversation|chat|message|turn|discussion|note|notes|details))\\b|(?=\\s*(?:$|[.!?)\\]])))`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep ordinary do-not-save phrases in the opt-out filter

The new object whitelist after save/store excludes common explicit instructions such as “Please don’t save any of this: I prefer working from Tokyo next month” and “Do not save or remember this: my temporary medical appointment is at 3 PM.” Both now return hasCaptureOptOut=false and shouldCapture=true in this exact module, whereas the previous broader pattern rejected them. Because the hook uses the same predicate to suppress trailing assistant replies, the opted-out content and its echoes can reach analyze(). Extend the imperative grammar to cover these forms and add hook-level regression tests while retaining the ordinary developer-prose cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended the opt-out grammar to support compound verbs like save or remember and any-of targets like don't save any of this. Added unit and hook-level regression tests in commit 6d0049e.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 1, 2026 16:07

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two previously reported forms are now excluded correctly, but the combined pattern introduces another opt-out regression. Four focused opt-out tests passed using the current TypeScript source with Node type stripping; an offline hook probe still forwarded the explicit do-not-remember turn and its assistant acknowledgement to analyze(). Full package and live tests were not rerun.

/** Conversational opt-out patterns that instruct the assistant not to persist the turn. */
const OPT_OUT_PATTERNS = [
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:(?:remember|memorize|save|store|record|capture|keep)(?:\\s+or\\s+(?:remember|memorize|save|store|record|capture|keep))?)(?:\\s+(?:any(?:\\s+of)?(?:\\s+(?:this|that|these|those|it))?|anything(?:\\s+(?:here|from\\s+this))?|this|that|these|those|it|what\\s+i|my\\b|our\\b|(?:in|to)\\s+memory|the\\s+(?:conversation|chat|message|turn|discussion|note|notes|details))\\b|(?=\\s*(?:$|[.!?:;,)\\]])))`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve explicit remember opt-outs with other objects

Combining remember/memorize with the save/store object whitelist makes ordinary opt-outs such as “Do not remember the following: I prefer working from Tokyo next month.” and “Please don't remember where I live: 123 Example Street.” return hasCaptureOptOut=false and shouldCapture=true. Both were rejected at the previous head. The hook therefore leaves optOutActive false and forwards both the user content and the trailing assistant acknowledgement to analyze(); the offline hook probe produced one request containing both messages. Keep the broader object handling for explicit remember/memorize imperatives, or cover these forms without weakening the developer-prose exclusions, and add a hook regression.

@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

@ducnmm Kept the broader object handling for explicit remember and memorize imperatives while preserving save and store object constraints and developer-prose exclusions in commit feec570. Added unit and hook-level regression tests for remember directives with other targets.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 2, 2026 01:08

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previously reported remember opt-outs are fixed and the four focused source-level tests pass. Offline predicate and hook probes found a new regression: ordinary coordinated first-person statements are now classified as opt-outs and the hook drops both the statement and the assistant reply. Full package and live tests were not rerun.

/** Conversational opt-out patterns that instruct the assistant not to persist the turn. */
const OPT_OUT_PATTERNS = [
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:remember|memorize)(?:\\s+or\\s+(?:save|store|record|capture|keep))?\\b`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve coordinated first-person developer statements

The shared prefix accepts and/but, so broadening remember to any object now treats ordinary statements such as “I prefer Rust but don't remember when we adopted it.” and “We use Postgres and don't remember why we picked that version.” as instructions not to save the turn. Both return hasCaptureOptOut=true and shouldCapture=false here, while the previous head captured them. The hook also suppresses the trailing assistant response; an offline hook probe made zero analyze calls for the Rust preference plus its acknowledgement. Distinguish coordinated first-person prose from an imperative before marking the whole turn opted out, and add a regression alongside the standalone “I don't remember” case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 84bffe3. Broadened remember/memorize opt-out matching now requires a clause-initial or polite imperative boundary (CLAUSE_IMPERATIVE_PREFIX), while coordinated conjunctions (and, but, ,) only trigger when paired with an explicit persistence verb or deictic turn target (this, that, it, anything, the following, the conversation). Added unit and hook-level regressions for coordinated first-person statements like "I prefer Rust but don't remember when we adopted it." and "We use Postgres and don't remember why we picked that version.".

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 2, 2026 06:40

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coordinated first-person examples now remain capturable, and the four focused source-level tests pass, including the earlier opt-out and assistant-echo regressions. An offline predicate/hook probe found a new boundary regression: an explicit privacy-directed remember opt-out after a comma now forwards the user content and assistant acknowledgement to analyze(). Full package and live tests were not rerun.

];

const CLAUSE_IMPERATIVE_PREFIX =
"(?:^|[.!?;:(\\[\\n]\\s*|\\b(?:please|just)\\s+|\\byou\\s+(?:must\\s+|should\\s+|can\\s+)?)";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve explicit remember opt-outs after a comma

Removing the comma boundary from broad remember matching means “For privacy, don't remember my home address: 123 Example Street.” and “For privacy, do not remember where I live: 123 Example Street.” now return hasCaptureOptOut=false and shouldCapture=true. The fallback pattern also excludes my and where I, so neither explicit instruction is recognized, although both were excluded at the previous head. A hook probe for the first example now makes one analyze() call containing both the address and the assistant's acknowledgement, versus zero previously. Preserve unambiguous privacy-directed imperatives after a comma while retaining the coordinated-prose exclusions, and add a hook regression for this form.

@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 93ddbf0: preserved the comma boundary in clause imperatives and added my, our, and where/what/who/how I to the remember fallback pattern while keeping coordinated-prose exclusions intact. Added unit tests for privacy-directed imperatives after commas and a hook-level regression confirming assistant echoes are suppressed.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 3, 2026 01:44

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The privacy-directed comma examples and the four focused opt-out tests pass. An offline comparison with the previous head found the new conjunction regression below; the hook forwards both the opted-out user content and its assistant acknowledgement to analyze(). No live requests were performed.

const CLAUSE_IMPERATIVE_PREFIX =
"(?:^|[.!?;:,(\\[\\n]\\s*|\\b(?:please|just)\\s+|\\byou\\s+(?:must\\s+|should\\s+|can\\s+)?)";

const IMPERATIVE_NEGATION_PREFIX = CLAUSE_IMPERATIVE_PREFIX;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve explicit opt-outs after conjunctions

Aliasing this prefix removes the previous and/but boundaries from the narrow fallback patterns. Consequently, My temporary medical appointment is at 3 PM and do not remember this. and My home address is 123 Example Street but do not save this in memory. now return hasCaptureOptOut=false and shouldCapture=true; both were excluded at the previous head. A focused hook probe makes one analyze() call containing the user message and the trailing assistant acknowledgement. Retain conjunction handling for explicit persistence verbs/deictic opt-out targets while preserving the coordinated first-person developer-prose exclusions, and add hook-level regressions for these forms.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 3, 2026 02:55
@harrymove-ctrl
harrymove-ctrl force-pushed the fix/openclaw-capture-optout-and-secrets branch from 93ddbf0 to 0759d2e Compare November 3, 2026 03:14
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

Fixed in 1dd3efdf: restored and and but conjunction matching in IMPERATIVE_NEGATION_PREFIX for the persistence-verb (save|store|record|capture|keep), log, and deictic-object remember|memorize opt-out patterns while keeping CLAUSE_IMPERATIVE_PREFIX for the broad clause-start remember|memorize pattern. Coordinated opt-outs such as "My temporary medical appointment is at 3 PM and do not remember this." and "My home address is 123 Example Street but do not save this in memory." now return hasCaptureOptOut=true and shouldCapture=false while coordinated first-person developer prose like "I prefer Rust but don't remember when we adopted it." remains capturable. Added unit and hook-level regression tests.

@harrymove-ctrl
harrymove-ctrl force-pushed the fix/openclaw-capture-optout-and-secrets branch from 1dd3efd to e9e3d39 Compare November 4, 2026 19:09
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

Rebased onto dev cleanly. All 50 OpenClaw tests pass. Ready for review.

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior opt-out grammar regressions are fixed and the focused opt-out suite passes (4 tests). The separate secret-echo path below still forwards a detected credential to extraction. Verification was offline; no live writes were performed.

const msg = messages[i];
if (!msg || typeof msg !== "object") continue;
if (msg.role === "user") {
optOutActive = hasCaptureOptOut(rawMessageText(msg));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Suppress assistant echoes of secret-bearing user turns

The new turn-level prefilter suppresses assistant replies only when the user has an opt-out directive. A user message detected by looksLikeSecret is instead dropped later by the individual-message filter, while a paraphrased assistant echo remains capturable. On this head, Staging test account uses password: hunter2 for integration runs is rejected, but the reply For integration testing, your staging database password is hunter2. passes shouldCapture; an offline call through the actual registerCaptureHook sends that reply, including hunter2, to analyze(). This defeats the added inline-secret exclusion whenever the assistant repeats a credential without its assignment syntax. Use the same turn suppression for a detected-secret user message until the next normal user turn, and cover the assistant-echo path in the hook test.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 5, 2026 05:13
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

Addressed in commit f96d82b:

  1. Updated extractNonOptedOutMessages in packages/openclaw-memory-memwal/src/hooks/capture.ts so user turns matching looksLikeSecret also suppress trailing assistant replies until the next normal user turn.
  2. Verified with pnpm --dir packages/openclaw-memory-memwal exec tsc && node --test packages/openclaw-memory-memwal/test/capture-optout.test.mjs packages/openclaw-memory-memwal/test/security.test.mjs packages/openclaw-memory-memwal/test/capture-cache.test.mjs (18/18 passing).

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previously reported password-bearing user turn now suppresses its assistant echo, and the next normal turn remains capturable. Session-cookie headers are still missed by secret detection, so their turns and assistant echoes reach analyze. The TypeScript build and all 18 focused capture, security, and cache tests pass. The additional actual-hook probe used a fake analyze client and synthetic cookie values, with no live requests.

/\b(?:glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{36}|[sprk]k_(?:live|test)_[A-Za-z0-9]{16,}|hf_[A-Za-z0-9]{30,})\b/,
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
/[A-Za-z][A-Za-z0-9+.-]{0,30}:\/\/[^\s/@:?=&]+:[^\s/@?=&]+@/,
/\b(?:(?:proxy-)?authorization\s*[:=]\s*(?:bearer|basic|token|digest)\s+\S+|bearer\s+[A-Za-z0-9._~+/=-]{20,})/i,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Detect session-cookie headers as secret-bearing turns

The header rule covers Authorization/Bearer but no Cookie or Set-Cookie form. For staging, send Cookie: sid=synthetic-session-cookie-value when reproducing dashboard login. therefore returns looksLikeSecret=false; an actual registerCaptureHook probe forwards that header and the assistant's paraphrased session value to analyze(). Set-Cookie: sid=synthetic-set-cookie-value; HttpOnly; Secure has the same result. Because the new turn suppression relies on this predicate, it cannot protect either the original cookie or its echo. Recognize Cookie/Set-Cookie headers before turn filtering and cover both outgoing hook payloads, while retaining ordinary cookie-related prose without credential values.

@harrymove-ctrl
harrymove-ctrl force-pushed the fix/openclaw-capture-optout-and-secrets branch from f96d82b to 24aa040 Compare November 6, 2026 17:18
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

@ducnmm Rebased onto dev and added session-cookie header patterns (Cookie and Set-Cookie) and session tokens to SECRET_PATTERNS in 24aa040. Verified that all 50 OpenClaw unit, hook, and suppression tests pass, including session-cookie user turns and assistant echo suppression.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 6, 2026 17:18

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline inspection and synthetic probes found an automatic-save privacy issue. No live upload was performed.

/(?:\b|(?<=[a-z0-9_-]))(?:session[_-]?cookies?|session[_-]?ids?|connect\.sid)\s*[:=]\s*(?:["'`](?!\s*["'`])[^"'`\n]{4,}["'`]|(?!(?:string|number|boolean|null|undefined|none|true|false)\b)[^\s'",;}{)\]]{6,})/i,
/(?:\b|(?<=[a-z0-9_-]))(?:passwords?|passwd|pwd|db[_-]?pass(?:word)?s?|passphrases?|api[_-\s]?keys?|apikeys?|secret[_-\s]?keys?|access[_-\s]?keys?|encryption[_-\s]?keys?|client[_-\s]?secrets?|secrets?|access[_-\s]?tokens?|refresh[_-\s]?tokens?|auth[_-\s]?tokens?|bearer[_-\s]?tokens?|session[_-\s]?tokens?|api[_-\s]?tokens?|private[_-\s]?keys?|priv[_-\s]?keys?|delegate[_-\s]?(?:private[_-\s]*)?keys?)["'`]?\s*[:=]\s*(?:["'`](?!\s*["'`])[^"'`\n]{4,}["'`]|(?!(?:string|number|boolean|null|undefined|none|true|false|required|optional)\b)[^\s'",;}{)\]]{6,})/i,
/\btoken["'`]?\s*[:=]\s*(?:["'`](?!\s*["'`])[^"'`\n]{8,}["'`]|(?!(?:string|number|boolean|null|undefined|none|true|false|required|optional)\b)[^\s'",;}{)\]]{12,})/i,
/(?:private[\s_-]*key|secret[\s_-]*key|delegate[\s_-]*(?:private[\s_-]*)?key|signing[\s_-]*key|api[\s_-]*key|seed)\b[^.\n]{0,32}\b(?:0x)?[0-9a-fA-F]{64}\b/i,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Detect labelled private keys across Markdown line breaks

The labelled-hex rules exclude newlines. Both # Delegate private key\n + 64 synthetic a characters and My delegate private key is\n + that value return looksLikeSecret=false and shouldCapture=true on this head. They therefore bypass the new secret-bearing-turn suppression and remain eligible for analyze(). This is a normal Markdown key layout. Match labels across line breaks before capture, and add a regression for the complete user/assistant turn; ordinary unlabelled public identifiers can stay capturable.

@harrymove-ctrl

harrymove-ctrl commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

@ducnmm Updated in commit 9dab3c5: matched labelled private keys across Markdown line breaks before capture in packages/openclaw-memory-memwal/src/capture.ts, and added regression tests for multiline labelled keys and assistant echo suppression in test/capture-optout.test.mjs.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 9, 2026 01:11

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiline labelled-secret fix passes the TypeScript build and 18 focused capture/security/cache tests, including the assistant-echo regression. One nonblocking changelog metadata cleanup remains below. No live upload was performed; this is not an approval or a claim that CI is green.

Comment thread docs/openclaw/changelog.mdx Outdated
- When was the MemWal OpenClaw plugin first released?
answer: >-
Version 0.0.9 prevents sub-agent namespaces and legacy aliases from colliding with the main agent's defaultNamespace, rejects sub-agent memory_search and memory_store requests for the main namespace, and updates the MemWal SDK dependency to 0.1.13. Version 0.0.8 bounds and expires idle capture-session state, watermarks auto-capture per session and preserves message timestamps for occurredAt, aborts timed-out analyze requests without blind retries, distinguishes accepted async remember jobs from completed storage in memory_store, updates the MemWal SDK dependency to 0.1.10, and detects letter-spaced prompt injection keywords.
answer: >-

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Remove the nested answer marker from the folded summary

This answer: >- is already inside the top-level folded answer value, so YAML treats it as literal summary text. Parsing the frontmatter produces a summary beginning with answer: >- rather than the version description. Remove this redundant marker and keep the summary indented directly under the original field.

@harrymove-ctrl
harrymove-ctrl requested a review from ducnmm November 9, 2026 04:07
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

@ducnmm Removed the redundant nested answer: >- line in docs/openclaw/changelog.mdx frontmatter in commit 9b1fc20e and verified all 18 OpenClaw tests pass.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants